fix(dev): install hooks during worktree bootstrap - #11496
Conversation
|
Re-run against Template looks good ✓ Problem: observed, not theoretical, and it is now evidenced three independent ways. Direction: aligned. Internal developer tooling — no product surface, and nothing touching auth, sandbox, model selection, telemetry, release, or a public contract. The only caller of this script outside its own tests is Size: not applicable — no core path is touched (Stage 0 does not fire), and no Stage 1e high-risk path matches. 99 production lines ( Approach: the two new commits are One thing worth saying plainly, since it is the honest read of the round count: this PR went through two Criticals and eleven Suggestions across four heads, and AGENTS.md asks that after roughly five rounds only Critical fixes land. Both new commits are defensible under that rule — one answers a Critical, the other a Suggestion whose wrong-signal shape was measured — but this is the point where further rounds should move to a follow-up issue rather than widen this diff. That is now moot for merging and still useful for the residuals. Risk: no elevated risk signals. Reviewed what landed. 🔍 中文说明针对 模板完整 ✓ **问题:**已观测到,不是理论性的,而且现在有三重独立证据。 **方向:**对齐。纯内部开发工具——没有产品界面,也不涉及 auth、sandbox、模型选择、telemetry、发布或对外契约。除自身测试外,这个脚本唯一的调用方是 **规模:**不适用——没有触及核心路径(Stage 0 不触发),Stage 1e 的高风险路径也没有命中。99 行生产代码( **方案:**两个新提交是 有句话该直说,因为这是对轮次的诚实判断:这个 PR 在四个 head 上经历了两条 Critical 和十一条 Suggestion,而 AGENTS.md 要求大约五轮之后只落地 Critical 修复。这两个新提交在该规则下都站得住——一个回应 Critical,另一个回应的 Suggestion 其错误信号形态是被实测过的——但这已经到了「后续轮次应该转成跟进 issue、而不是继续加宽这个 diff」的节点。对合并而言这已经无所谓,对残留项仍然有用。 **风险:**无升级风险信号。 已审查落地的内容 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewReview of No reuse path was skipped, worth saying because Both Criticals are fixed, and I checked each against the landed file rather than the thread replies. R1-7 (original) — husky's write escaping the bootstrapped worktree. R1-7 (fix-induced) — the I also re-derived the cross-component constraint rather than reusing last round's note. The eleven Suggestions are all closed, and three of them are closed by tests that are better than the fix required. The two new failure conjuncts at The exit-128 arm is the one I'd most have pushed on if it were weak, and it isn't: it asserts status 1, that stdout does not contain I re-checked Findings on what landedNone of these blocked the merge — it already happened — so they are follow-ups. Two are worth a maintainer's attention, and I'd put the first one at the top of the list. 1. The fix is two lines: hoist 2. Two things make this more than a hypothetical, and both are new this round. The repo already treats an ambient Why the suite cannot catch it: every arm of the ownership fixture sets One caution on the obvious fix: 3. The design doc is still English-only. 4. Nit — the artifact witness hardcodes one hook name. 5. Nit — the Windows arms of the fixture stay unexercised on 6. Nit — one message can misattribute its own cause. CI test evidenceUnattended CI run — I built nothing, executed nothing, and ran no PR-derived code. Everything below is this PR's own check results for the reviewed commit, read through the API: 24 check-runs, zero failures, so there is no failing-job log to quote. The lane that matters most for the newest commit is green on all three platforms. What no lane covers is worth stating exactly, because it is the arm this PR exists for. The smoke job runs against a fresh
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。
Sandboxed verification would settle what changed since the last A/B: Not verified: the linked-worktree and no-repository arms with real husky (no lane pairs them, as above); the fixture's Windows batch-stub arm and its POSIX-gated exit-128 arm (that lane is off for PRs); git's actual exit code for a dubious-ownership refusal, which I took from the author's test comment rather than measuring; and finding 2's ambient-config door, which 中文说明代码审查针对 没有跳过任何可复用的实现,这一点值得说,因为 两条 Critical 都已修复,而我是对照落地的文件核实的,不是采信线程里的回复。 R1-7(原始)——husky 的写入逃出被初始化的 worktree。 R1-7(修复引入)——用 我也重新推导了跨组件约束,而不是复用上一轮的笔记。 十一条 Suggestion 全部关闭,其中三条的关闭方式比修复本身要求的更好。 exit-128 分支是如果薄弱我就会去推的那一个,而它不薄弱:它断言退出码 1、stdout 不含 移除 对落地代码的发现这些都没有阻塞合并——合并已经发生——所以它们是后续项。两条值得维护者关注,而我会把第一条放在最前面。 1. 修复是两行:把 2. 有两点使它不只是假设,而且都是本轮新增的。仓库已经把环境里的 为什么测试套件抓不到它:归属夹具的每一个分支都设置了 对那个显而易见的修复的一点提醒: 3. 设计文档仍然只有英文版。 4. 小问题——产物见证硬编码了一个 hook 名。 5. 小问题——夹具的 Windows 分支在 6. 小问题——有一条信息会误报自己的成因。 CI 测试证据无人值守的 CI 运行——我没有构建任何东西、没有执行任何东西,也没有运行任何来自 PR 的代码。下面全部内容都是这个 PR 自己的 check 结果,针对被审查的 commit 通过 API 读取:24 个 check-run,零失败,所以没有失败 job 的日志可引用。 对最新提交最重要的那条 lane 在三个平台上都是绿的。 没有任何 lane 覆盖的部分值得精确说明,因为那正是本 PR 存在的分支。smoke job 跑在全新的
沙箱验证可以定论自上次 A/B 以来变化的部分: 未验证:使用真实 husky 的 linked-worktree 与无仓库分支(如上,没有 lane 把它们配在一起);夹具的 Windows 批处理桩分支和被限制在 POSIX 的 exit-128 分支(该 lane 对 PR 关闭);git 对 dubious-ownership 拒绝的实际退出码,这一点我取自作者的测试注释而非亲自测量;以及发现 2 的环境配置之门, — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 4/5 — the code that landed is solid: both Criticals are verifiably fixed and pinned by tests that would go red if the guards were removed, and CI is fully green at this commit including real husky on all three platforms. What keeps it off 5 is two residual doors, one of them newly introduced by the last commit and now live on There is no gate action available, so I am not taking one. This PR merged at 09:45:05Z, seven minutes after the Stepping back over the whole arc rather than the last delta: this PR started as a hooks-not-installed bug with a real CI log behind it, and the first fix introduced a worse bug than the one it closed — husky's unguarded If I pick this up in six months I thank the author. The guard is explained by a comment that names the mechanism it is defending against, including why The two things I would want done next, in order:
The guard reads the effective One process note, offered neutrally because it is the reason this comment reads as a post-mortem: the standing review decision at merge time was @yiliang114 the two follow-ups above are yours to schedule — the first is small enough to land on its own, and the second is worth an issue since it needs a new fixture axis. 中文说明信心度:4/5 — 落地的代码是扎实的:两条 Critical 都被可验证地修复,并由「守卫被移除就会变红」的测试钉住;CI 在这个 commit 上全绿,包括三个平台上真实的 husky。让它到不了 5 分的是两扇残留的门,其中一扇由最后一个提交新引入,现在已经在 没有任何可用的门禁动作,所以我一个也不做。 本 PR 已于 09:45:05Z 合并,就在 跳出最后的增量、回看整条弧线:这个 PR 起点是一个有真实 CI 日志支撑的「hooks 未安装」bug,而第一次修复引入了一个比它所关闭的更糟的 bug——husky 无保护的 如果六个月后我接手这块代码,我会感谢作者。这道守卫由一段点名了它所防御机制的注释解释,包括为什么 接下来我希望被做的两件事,按顺序:
守卫读的是生效的 一条流程说明,中立地提出,因为它正是本条读起来像事后分析的原因:合并时的常驻 review 决定是 @yiliang114 上面两条后续项由你来排期——第一条小到可以独立落地,第二条值得开一个 issue,因为它需要一条新的夹具轴。 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI for this PR (they are gated on merge_group/schedule/workflow_dispatch), so the changed script suite never executed on those platforms, and this review's own build-test scoped to zero workspaces so it never ran that suite locally either (the Linux Test lane did run it and passed).
Not explored to full depth (tool budget reached): "agent 1d": none — I completed the walk and the lane checks; no candidate was cut short..
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
中文说明
仅完成部分审查,审查缺口已披露。
未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI for this PR (they are gated on merge_group/schedule/workflow_dispatch), so the changed script suite never executed on those platforms, and this review's own build-test scoped to zero workspaces so it never ran that suite locally either (the Linux Test lane did run it and passed).
未探索到全部深度(达到工具调用预算):"agent 1d":none — I completed the walk and the lane checks; no candidate was cut short.。
未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
— qwen3.8-max via Qwen Code /review (v0.23.2)
Husky runs `git config core.hooksPath .husky/_` with no --worktree, so from a linked worktree the value lands in the config every worktree of the repository shares while `.husky/_` is created only in the checkout being bootstrapped. Skip the Husky step and report it when the key is unset and this checkout does not own the repository config, so a bootstrap can no longer repoint hook resolution for roots that never received the wrappers. A primary checkout still installs hooks, and an already-configured `core.hooksPath` is untouched. Also drop the caller's success exit, which `install()` made unreachable when it started exiting on every successful path, and bring the pnpm-worktree-bootstrap design doc in line with a hook step it still recorded as deliberately skipped. The new fixture runs the real script against a throwaway root whose `.git` is a file or a directory and whose config comes from a real `git init` repo, which makes both new branches reachable and pins the fail-closed guard: the injected `GIT_CONFIG_*` constant holds one value for the child's whole lifetime and cannot express the unset state that asks husky to write. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtuo29vduf
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 118 passed · 0 failed · 118 total Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:118 通过 · 0 失败 · 118 总计 抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #11496 — deep verification reportVerdict: Verified head: 中文摘要
Scope selection
Everything else is listed under Not covered. Central claim — A/B against the base buildOne fixture repository per cell, built once and copied per cell; every cell runs the real
The load-bearing pair is 01/02: identical environment, identical traced commit, Corrections to the PR descriptionThe stated build/typecheck blocker does not reproduce at the verified head. The body says: "The full build currently fails because the existing document-export runtime exceeds its size budget by 15,245 bytes; typecheck then cannot resolve the missing generated web-template output." Measured in this lane at
This is a correction to the description, not a request to change code: the author used that blocker to justify skipping build/typecheck, and at this head neither gate is blocked. (The number presumably reflects the author's local main at writing time; the warn/fail distinction, however, is structural.) FindingsF1 — commit 2's guard keys on the effective
|
| cell | config source of .husky/_ |
repository config after | bystander hook in primary (before → after) |
|---|---|---|---|
| C-head | unset everywhere | unset | 1 → 2 (alive) |
| C-M3 (guard removed) | unset everywhere | written | 1 → 1 (dead) |
| D-head | worktree config.worktree (extensions.worktreeConfig=true) |
written | 1 → 1 (dead) |
| D-patched | same | unset | 1 → 2 (alive) |
| E-head | global config | written | 0 → 0 (already redirected before the bootstrap) |
| E-patched | same | unset | 0 → 0 (same) |
In D the bystander oracle is a plain .git/hooks/pre-commit planted in the primary checkout: after bootstrapping the linked worktree, the primary's own hook silently stops firing while its commits keep succeeding (exit 0) — the exact harm commit 2's guard exists to prevent, reached through a door the guard does not cover. Reachability is narrow and I measured it as such: nothing in this repository sets extensions.worktreeConfig in a developer checkout (the workflows only sanitize it away as a known hazard, e.g. .github/workflows/qwen-triage.yml:2748), and a global core.hooksPath of exactly .husky/_ is unusual. Base never writes the config in either configuration (base never runs husky), so this is new surface introduced by the PR, in configurations base also mishandled differently (by leaving the worktree hookless).
Reproduce:
D=tmp/pr11496-verify-20260910-002421
node $D/ab-harness.mjs $D # builds the fixture template
BH_SRCS=head node $D/bystander-harness.mjs $D # cells D-*/E-*
# read-scope probe that names the mechanism:
git -C <linked> config --worktree core.hooksPath .husky/_
git -C <linked> config --get core.hooksPath # -> .husky/_ (what the guard reads)
git -C <linked> config --local --get core.hooksPath # -> unset (what husky writes)Measured minimal fix (candidate, not applied to the PR)
Ask the file husky writes, not the effective value, in the guard only — setup-worktree.patched.js in this artifact dir, prettier- and eslint-clean:
+function getSharedHooksPath() {
+ const result = spawnSync(
+ 'git',
+ ['config', '--local', '--get', 'core.hooksPath'],
+ { cwd: rootDir, env, encoding: 'utf8' },
+ );
+ return result.status === 0 ? result.stdout.trim() : undefined;
+}
...
- if (hooksPath === undefined && !ownsRepositoryConfig()) {
+ if (getSharedHooksPath() === undefined && !ownsRepositoryConfig()) {Three measured results:
- Hostile fixtures go clean:
BH_SRCS=head,patched→ D/E-patched leave the repository config unset, printskipping Husky, and the bystander stays alive (1 → 2). 52/52 expectations pass. - Zero collateral: the patched source through all 12 benign A/B cells produces byte-identical outcomes — 56/56 pass, table identical (
ab-results-patched.jsonvsab-results-head.json). - Suite counts unchanged:
npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/package-scripts.test.jsgives40 passed | 1 skippedboth with and without the patch — i.e. the suite pins nothing along this axis. The fixture that would pin it is a fourth case ininstalls hooks only where the checkout owns the repository config: set the value withgit config --worktree(or viaGIT_CONFIG_GLOBAL, which that test currently pins to an empty file — exactly why it cannot see door E) while the repository config is unset, and assert the repository config stays unset and stdout reports the skip.
Tradeoff to state plainly: with the patch, D/E worktrees get no wrappers (their hooks stay silent) — the same tradeoff commit 2 already accepts for the unset-key case, applied consistently.
F2 — new hard failure outside a git repository (informational)
With no .git at all, head exits 1 with worktree setup failed: Husky did not install hooks where base exits 0 (cells 11/12). The mechanism is real husky's own silent-success path: husky 9.1.7 returns the string .git can't be found and still exits 0, so the fail-closed check fires. The cause survives — husky's reason is printed on stdout above the script's message — so diagnosability is intact, and "fail closed" is a defensible choice for a bootstrap that is documented for worktrees only. The same conflation means a broken git config --get (any non-zero exit) reads as "unset": in a primary that then runs husky, whose write also fails, the operator sees husky's stderr followed by the fail-closed message. Reported as informational, not a defect: getHooksPath() returns one sentinel for "unset" and for "could not measure", but every consumer still ends in an observable outcome.
F3 — design doc has no Chinese counterpart (Suggestion, pre-existing gap)
docs/design/2026-08-29-pnpm-worktree-bootstrap.md was updated; docs/design/2026-08-29-pnpm-worktree-bootstrap.zh-CN.md does not exist (the doc was created without one). AGENTS.md requires both languages with reciprocal links and synchronised updates. Per this repo's review rules a translation gap alone is a Suggestion, and it predates this PR — but this PR widened the gap by adding English-only prose about the new hook behaviour.
F4 — one Reviewer Test Plan step is configuration-dependent (Suggestion, plan text)
Step 1 of How to verify — "Create a fresh linked worktree, run the documented pnpm worktree bootstrap, and confirm that the pre-commit hook is generated and executable" — passes only when the shared config already carries core.hooksPath=.husky/_ (cell 02). In a checkout where the key is unset the bootstrap deliberately skips husky and prints why (cell 03), so the step cannot be performed as written. Notably, this lane's own checkout is in the unset state: .husky/_ exists (mtime 00:11:07, from an earlier job's install, whose husky wrote the config) while .git/config was rewritten at 00:17:19 by actions/checkout with no core.hooksPath — so a maintainer following the plan in a CI-like checkout would hit the skip. The plan should name the precondition; the code's behaviour is commit 2's intent.
Full plan walk: step 1 → passes in the reported configuration (02), not reproducible in the unset configuration (03, see above); step 2 ("custom hooks path and HUSKY=0 are left unchanged") → passes (05, 06: values preserved byte-identical, no husky invocation, no wrappers); body claim "fails if Husky reports success without configuring the expected hooks path" → passes (09 with a stub husky, 11 with real husky's own silent-success path); body claim about tree cleanliness → passes (02, 07).
Mutation matrix and vacuity
mutants.mjs writes each mutant into the real working tree and runs the PR's own suite; witness evidence/02-mutation-matrix-killed-vs-survived.png, detail in mutants/matrix.json.
| mutant | what | suite |
|---|---|---|
CTRL whole-file revert to HEAD^1 |
positive control | KILLED — 3 tests red on their intended assertions (package-scripts.test.js:261 missing pnpm exec husky log line, :385 missing skipping Husky, :523 missing husky line in the registry-fallback case) |
| M0 husky never invoked | control | KILLED — same 3 |
M1 drop HUSKY === '0' clause |
guard | KILLED — :266 (invocation log) |
| M2 drop custom-path clause | guard | KILLED — :269 |
| M12 drop both early-exit clauses | combination | KILLED — :266 (no hidden layering: both singles die alone) |
| M3 drop linked-worktree guard | commit 2's guard | KILLED — :385 |
| M4 drop fail-closed check | guard | KILLED — :411 |
M5 ownsRepositoryConfig() always true |
guard | KILLED — :385 |
M6 ownsRepositoryConfig() always false |
guard | KILLED — :395 |
M7 revert the pathValue→envValue extraction |
refactor | SURVIVED (40 pass) |
M8 fail-closed compares '.husky' |
guard | KILLED — 3 tests |
| M9 drop the skip message | observability | KILLED — :385 |
9/10 mutants killed; the two controls prove the harness can make the suite fail. The single survivor M7 is correctly classified as not a coverage gap: base's pathValue() already handled the Windows Path casing, so the extraction is behaviour-preserving on every platform; the newly observable behaviour it enables (case-insensitive HUSKY read) is covered by the win32-only test resolves the path variable under its native Windows casing, which is skipIf(platform !== 'win32') and therefore never runs here. Nothing to write.
The suite's oracles differ from production's. M1 and M2 are killed by the invocation log and by a nonzero exit respectively, because the suite's corepack is a stub that logs and exits 0 without ever running husky. With real husky the consequences are different and worse-shaped: M1 turns the supported HUSKY=0 opt-out into a hard failure (exit 1 via the fail-closed check, cell A), and M2 silently clobbers an operator's /custom/hooks to .husky/_ while exiting 0 (cell B). The guards are load-bearing in reality; the suite pins them by a proxy. Recorded as completeness reporting, not a merge condition.
Vacuity: reverting scripts/setup-worktree.js to HEAD^1 turns exactly 3 tests red, each failing on the behavioural assertion it exists to catch (quoted above), not on an import/compile error; the file was restored byte-identical (sha256 68a8f4ec…).
Targeted gates
npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/package-scripts.test.js→ 40 passed | 1 skipped (41), 1.17 s. The skip is the win32-only test.npm run test:scripts(whole scripts workspace) → 82 files, 2187 passed | 15 failed | 1 skipped. Both failing files are environmental and outside this PR's import closure (neither referencessetup-worktree):scripts/tests/check-tui-dep-direction.test.jsfails withEACCES: permission denied, mkdtemp '…/.qwen/gate-e2e-XXXXXX'—.qwenis root-owned modedr-xr-xr-xwhile the suite runs as uid 1000, and the samemkdtempSyncfails with no PR code involved (A/A probe, quoted intest-scripts-full.log);scripts/tests/install-script.test.jsfails ondocker buildwith docker absent from the container. The PR's own file passed inside this run.- Prettier and ESLint on the three changed files → clean, each proven live by a planted violation that was reported and then restored byte-identical (sha256 verified); 10/10 scripted gate assertions in
gates-results.json. npm run build→ exit 0;npm run typecheck→ exit 0 (see Corrections).
Not covered
- Windows and macOS. The win32-only test is skipped on Linux and no Windows host is available here; the
envValue()casing path and the smoke workflow'swindows-latest/macos-latestlegs are unexercised. - The real monorepo bootstrap. The fixture uses the real pinned pnpm, real husky and real git, but a single-package layout: the repo's own workspace graph,
patches/,postinstall: patch-package, andpnpm-workspace.yamlsettings are not part of any cell. pnpm-worktree-smoke.ymlend-to-end on a runner. Cell 07 reproduces its input state (fresh primary checkout, key unset) and its clean-tree assertion; the workflow itself was not executed.- Per-commit attribution. The checkout is shallow (
git rev-parse --is-shallow-repository= true) andgit rev-list --count HEAD^1..HEAD^2returns the shallow-boundary trap value1against the snapshot's 2 commits; commit 1 (5416b451) is not locally reachable. The aggregateHEAD^1..HEADdiff is what was verified; commit 2's guard is isolated separately by mutants M3/M5/M6 and cells C/D/E. - The linked CI failure (
actions/runs/34353187365) — no GitHub token in this lane; not fetched. - Repo-wide
npm run testacross workspaces — the PR touches no package code; out of scope by design. - The full scripts suite's 15 failures were not re-run on a base worktree. They are attributed by mechanism (root-owned read-only
.qwen, absent docker) with an A/A probe of the failing operation itself, not by a base-side rerun. verify-capture.mjsworked as wired; no capture pipeline was reconstructed.
Methodology
Environment: the CI verify container (node:22-bookworm, node v22.23.2, git 2.39.5, corepack 0.34.6 resolving the repo-pinned pnpm 11.24.0), working tree = refs/pull/11496/merge at depth 2, npm ci + npm run build pre-run. The A/B fixture lives in /tmp/pr11496-fixture (outside the repository on purpose: cell 11/12 delete .git, and a fixture nested under this checkout would otherwise resolve every git config to the outer qwen-code repository); scratch git repos, real git worktree add linked checkouts, real corepack/pnpm installs against a warm store, and the real husky 9.1.7 package are the only things between the script under test and .husky/_/pre-commit — the sole stubs are the two cells that say so (09/10, husky replaced by a package that exits 0 configuring nothing) and the PR suite's own stub corepack, which is why the real-husky harness exists. Harnesses (ab-harness.mjs, bystander-harness.mjs, mutants.mjs, gates.mjs) and raw per-cell logs (cell-logs/, cell-logs-head/, cell-logs-patched/, build-at-head.log, typecheck-at-head.log, test-scripts-full.log) are in this directory; ab-results.json, mutant-real-results.json, mutants/matrix.json, gates-results.json carry every number quoted above.
Flakiness gate log
rounds=5 files=1 skipped=0
file scripts/tests/package-scripts.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/package-scripts.test.js
per-file results (P=pass F=fail I=infra-exit, one letter per run):
scripts/tests/package-scripts.test.js: PPPPP
verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/package-scripts.test.js: P (exit 0)
round 2 · scripts/tests/package-scripts.test.js: P (exit 0)
round 3 · scripts/tests/package-scripts.test.js: P (exit 0)
round 4 · scripts/tests/package-scripts.test.js: P (exit 0)
round 5 · scripts/tests/package-scripts.test.js: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅ The Critical from the previous round is fixed at this commit and pinned by a test that fails without it; CI is green on all lanes that run for PRs. The two open Suggestions (the config-only post-check and the git-unreadable misattribution) are recorded in my notes above as follow-ups, not blockers.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-1 fail-closed post-check re-reads core.hooksPath rather than verifying husky's wrappers — still stands, already reported (comment 3971820508)
- R1-9 fallback test's widened PATH dropped the no-ambient-tooling assertion — still stands, already reported (comment 3971820547)
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and the changed script suite never executed on those platforms; it ran on Linux in CI and locally (40 passed | 1 skipped).
Not explored to full depth (tool budget reached): "agent 3c": could not run eslint / prettier / vitest or read node_modules/husky to confirm husky v9's exact exit behaviour when .git is missing — node_modules is em….
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
scripts/setup-worktree.js:69 — [probe] Guard decides ownership from the filesystem while the config read and husky's write both follow the caller's GIT_DIR, so the repository reasoned about and the repository written are independent inputs
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and the changed script suite never executed on those platforms; it ran on Linux in CI and locally (40 passed | 1 skipped).
未探索到全部深度(达到工具调用预算):"agent 3c":could not run eslint / prettier / vitest or read node_modules/husky to confirm husky v9's exact exit behaviour when .git is missing — node_modules is em…。
未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.23.2)
`ownsRepositoryConfig()` inferred repository-config ownership from the
filesystem shape of `.git`, and the proxy is wrong at both ends. With no
`.git` at all, `statSync(..., { throwIfNoEntry: false })` returns `undefined`
and the predicate folded that absence into "owns the config", so a
repository-less checkout ran husky into its `.git can't be found` soft failure
(exit 0) and the fail-closed check then turned a successful dependency install
into exit 1 blaming Husky. A `.git` file is not only a linked worktree either:
`git clone --separate-git-dir` checkouts and submodules have one too and do own
their config, so hooks were declined where they would have been correctly
scoped.
Ask git instead: `rev-parse --git-dir` differs from `--git-common-dir` only in
a linked worktree, and a failed `rev-parse` names the no-repository state, so
the skip notice stops asserting "linked worktree" and the bootstrap does not
gain a hard git dependency.
Also bind the fail-closed check to an artefact husky's own write produced, not
only to the config value. husky 9.1.7 exits 0 on every soft-failure path
(`index.js:16` git command not found, `index.js:17` refused `git config` write)
before the `mkdirSync(_())` on line 19, and a linked worktree inherits
`core.hooksPath` from the config it shares, so re-reading that value compared
it against itself and passed exactly when husky had created nothing.
Restore the registry-fallback case's hermeticity: `PATH` holds only the stub
directory again, which now also pins that the retry needs no ambient git.
Rebuild the ownership fixture from real git layouts, because `rev-parse`
resolves nothing for a `mkdirSync`'d `.git` or a hand-written `gitdir:` file,
and give the stub husky a failing mode so the exit code husky returns is pinned
rather than the install result's.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtuys2rjuu
- getHooksPath() no longer collapses a refused git config read (exit 128/2/3) into "unset": only an absent key (exit 1) or a missing git binary keeps the skip path; anything else fails the bootstrap with the read error instead of a green, hook-less worktree. - The linked-worktree skip notice now names the recovery path: re-run this script once the primary checkout has hooks installed. - Tests cover a git stub exiting 128, the real-world unset HUSKY state, and pin the recovery sentence in the skip notice.
|
The red
Likely origin (attribution, not blame): No fix has landed yet. Live main head is now What this PR needs: nothing from the author's side. The check goes green once main is fixed and this branch re-runs or re-merges main. A bot review ( |
Pick up main's 2488d12 (fix(web-shell): realign two suites with shipped behavior to unbreak main CI, #11530). This branch's Test job failed only in packages/web-shell (WorkspaceSessionProvider.loading.test.tsx and WebShellSidebar.brand.test.tsx), which it does not touch -- those were main's own failures, already fixed on main. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtva7llqva
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 388 passed · 0 failed · 388 total Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:388 通过 · 0 失败 · 388 总计 抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #11496 — deep verification report (follow-up round)Verdict: Verified head: 中文摘要
Previous-finding statusThe previous substantive report verified
Scope selection
Everything else is listed under Not covered. Central claim — A/B against the base build45 cells in run 1 plus 10 in run 2, one throwaway repository per cell, all outside the checkout on purpose (cells 11/12 have no The load-bearing pair. Cells
Cell 01 reproduces the reported bug end-to-end: the bootstrap is green, the wrappers are absent, and the commit succeeds with empty stderr while invoking no hook — the silent-bypass shape, which is why this outranks a loud failure. Cells 23b/23 repeat the pair in the layout this repository's own tooling actually uses (a linked worktree created under Everything else measured, grouped by what it decides (
Two scalar facts settle questions no amount of reading settles, both measured here rather than inferred:
CorrectionsThe stated build/typecheck blocker still does not reproduce, and main has since changed what the budget measures. The body says: "The full build currently fails because the existing document-export runtime exceeds its size budget by 15,245 bytes; typecheck then cannot resolve the missing generated web-template output." Measured in this lane at
This is a correction to the description, not a request to change code. Note for anyone comparing rounds — the two measurements are not comparable, and the reason is measured rather than assumed: the previous round reported a single "document-export runtime" of 4,142,391 bytes against thresholds of 4,100,000 / 4,200,000 read from the same file. At this head those thresholds are 1,870,000 / 1,930,000 and the budgeted quantity is the renderer JS bundle alone, with the CSS printed beside it but not compared against either limit. Today's JS + CSS is 1,837,054 + 2,302,905 = 4,139,959, within 2,432 bytes of the previous round's combined figure, which is consistent with that round's number having been the two added together — so the basis changed on main between the two base tips, and neither the old byte count nor the old thresholds describe this head. The structural point survives unchanged and does not depend on any of that arithmetic: the author used this blocker to justify skipping build and typecheck, and at this head neither gate is blocked. FindingsF1 — the guard reads the effective
|
| cell | arm | source of the effective .husky/_ |
shared config after | bystander in primary |
|---|---|---|---|---|
| 19 | head | door D: config.worktree (extensions.worktreeConfig=true) |
WRITTEN | 1 → 0 DEAD |
| 19b | base | door D | untouched | 1 → 1 alive |
| 19p | patched | door D | untouched | 1 → 1 alive |
| 20 | head | door E: global config | WRITTEN | 0 → 0 |
| 20b | base | door E | untouched | 0 → 0 |
| 20p | patched | door E | untouched | 0 → 0 |
| 03 | head | control: unset everywhere | untouched | — |
The bystander is a plain .git/hooks/pre-commit planted in the primary checkout before the linked worktree is bootstrapped. In cell 19, after the bootstrap, the primary's own hook silently stops firing while its commits keep succeeding — hook resolution repointed for a root that never received the wrappers. That is precisely the harm commit 2's guard was written to prevent, reached through a door it does not cover. Door E's bystander is degenerate by construction (a global core.hooksPath already redirects the primary before the bootstrap runs), so its oracle is the shared-config write itself: the redirection becomes persistent in the repository rather than ambient.
Bounds, stated plainly. Base never writes the shared config in either door (base never runs husky), so this is surface the PR introduces. Reachability differs between the two doors and I measured both rather than averaging them:
- Door E (a global
core.hooksPathof exactly.husky/_) is unusual in a developer checkout, and nothing in this repository sets one. Narrow. - Door D (
extensions.worktreeConfig=trueplus a worktree-scopedcore.hooksPath) is a state this repository has already met..github/workflows/qwen-triage.yml:2748-2755documents it in the maintainers' own words — "extensions.worktreeConfig=true… activates.git/config.worktree— a second config file thatgit config --localneither lists nor unsets, and that CAN carry core.hooksPath. Verified: a prior run can set--worktree core.hooksPath=/, survive the sweep untouched" — and mitigates by deletingconfig.worktreeoutright. The same block appears three times inqwen-autofix.yml. So the configuration is not hypothetical in CI; it is one the bot sanitizes on sight. What keeps the exposure narrow is that no workflow runssetup-worktree.jsin a workspace it has not just created:pnpm-worktree-smoke.ymluses a freshactions/checkout(cell 07's state), and the bot's own workspaces go through the sweep above. A developer who enables worktree config locally is the realistic door-D path, and nothing in the repo suggests or forbids that.
No exploit or data loss in either door; the consequence is a silently hookless sibling checkout, which is the same class of harm the PR set out to close — and cell 19 shows it arriving with a green exit code, so nothing tells the operator.
Reproduce:
D=tmp/pr11496-verify-20260910-095805
node $D/ab-harness.mjs $D # runs all 45 cells
node $D/print-f1-doors.mjs $D # the table above
# the mechanism in three commands:
git -C <linked> config --worktree core.hooksPath .husky/_ # needs extensions.worktreeConfig=true
git -C <linked> config --get core.hooksPath # -> .husky/_ (what the guard reads)
git -C <linked> config --local --get core.hooksPath # -> exit 1 (what husky writes)Measured minimal fix (candidate, not applied to the PR)
Ask the file husky writes, not the effective value, in the guard only — patched-setup-worktree.js in this artifact dir, 13 additions / 1 deletion, prettier- and eslint-clean:
+function getSharedHooksPath() {
+ const result = spawnSync(
+ 'git',
+ ['config', '--local', '--get', 'core.hooksPath'],
+ { cwd: rootDir, env, encoding: 'utf8' },
+ );
+ return result.status === 0 ? result.stdout.trim() : undefined;
+}
...
- (hooksPath === undefined && ownership === 'linked')
+ (getSharedHooksPath() === undefined && ownership === 'linked')Three measured results (zero-collateral.txt, gates-results.json):
- Hostile fixtures go clean: cells 19p/20p leave the shared config
untouched, printskipping Husky, and door D's bystander stays alive (1 → 1). - Zero collateral: across all 13 benign cells the patched source produces identical outcomes on every one of 17 oracles — exit, wrappers, exec bit, hook fired, local/effective/worktree config values, shared-config write, tree cleanliness, all three notice flags, both fail-closed flags, stub invocation, ownership verdict, and both bystander counts. 13/13 identical, 0 differ.
- Suite counts unchanged:
40 passed | 1 skippedwith and without the patch — i.e. the suite pins nothing along this axis, which is why the mutation matrix'sFURTHER-FIXrow survives. The fixture that would pin it is a fourth case ininstalls hooks only where the checkout owns the repository config: set the value withgit config --worktree(or viaGIT_CONFIG_GLOBAL, which that test currently pins to an empty file — exactly why it cannot see door E) while the repository config is unset, then assert the repository config stays unset and stdout reports the skip.
Two tradeoffs to state rather than hide. First, with the patch, door D/E worktrees get no wrappers — the same tradeoff commit 2 already accepts for the unset-key case, applied consistently. Second, --local --get returns exit 128 outside a repository (measured: cell 11 local=unset(exit 128)), so a patch written this way re-introduces, on the local read, the same "unset vs could not measure" conflation commit 4 just removed from the effective read; a version meant to land should carry commit 4's exit-code discrimination over to getSharedHooksPath() too. The cells above pass either way because the ownership === 'none' branch short-circuits first.
N1 — HUSKY=0 does not survive a refused config read, so the opt-out the PR body promises is defeated in exactly the configuration commit 4 was written for (Suggestion, new at this head)
install() reads the hooks path before it looks at the opt-out:
const hooksPath = getHooksPath(); // can console.error + process.exit(1)
if (envValue('HUSKY') === '0' || (hooksPath !== undefined && hooksPath !== '.husky/_')) {Commit 4 made getHooksPath() exit 1 on any status other than 0, 1, or a spawn error. The two facts combine: when git answers but refuses the read, the bootstrap hard-fails before the HUSKY === '0' branch is reached. The PR body states the change "respects the HUSKY=0 opt-out"; measured, it does not in this configuration.
| cell | arm | config read | HUSKY |
exit | stderr |
|---|---|---|---|---|---|
| 18 | head | refused (git stub exits 128 for this one invocation only) | 0 |
1 | worktree setup failed: could not read core.hooksPath (fatal: simulated refused config read) |
| 18b | base | refused | 0 |
0 | — |
| 06 | head | normal | 0 |
0 | — (husky never invoked) |
Cell 16 shows the same exit is correct when hooks are actually wanted (corrupt .git/config, no HUSKY opt-out → fail loud rather than ship a green hookless checkout). The defect is only the ordering: an operator who has explicitly said "no hooks" is failed by a read whose result they never asked for. The two conditions plausibly co-occur — commit 4's own message names "a shared pool's dubious-ownership exit 128", and HUSKY=0 is what an operator sets on a shared host.
Measured minimal fix (candidate, not applied to the PR)
patched2-setup-worktree.js, 6 additions / 4 deletions, prettier- and eslint-clean:
if (result.status === 0) {
+ if (envValue('HUSKY') === '0') {
+ exitWithResult(result);
+ }
const hooksPath = getHooksPath();
- if (
- envValue('HUSKY') === '0' ||
- (hooksPath !== undefined && hooksPath !== '.husky/_')
- ) {
+ if (hooksPath !== undefined && hooksPath !== '.husky/_') {
exitWithResult(result);
}Measured (ab-results-2.json, cells H02–H18):
- Target cell flips: H18 → exit 0, no
could not readmessage, husky never invoked. - Zero collateral: H16 still exits 1 with
could not read core.hooksPath(the loud path survives when hooks are wanted); H02 wrappers + hook fired; H03 skip notice + recovery sentence; H05/custom/hookspreserved; H06 opt-out still never invokes husky; H07 wrappers + hook fired; H09 still fails closed. 8/8 cells as expected — 36/36 assertions across them, within run 2's 50/50 (the remaining 14 are the M11 pair, cells 24/25). - Suite counts unchanged:
40 passed | 1 skippedwith the patch applied. As with F1, that means the axis is unpinned — the fixture that would pin it is the existing exit-128 case attest.js:526run a second time withHUSKY: '0'in the environment, asserting exit 0 and nocould not readmessage.
Both candidate fixes were also put through the repository's own house-style gates in the location the config governs (style-gates.mjs, 7/7): written into scripts/setup-worktree.js, npx prettier --check and npx eslint both exit 0, then the file is restored byte-identical (sha256 d45c4975…) and git status --porcelain is empty. Those two tools were proven live earlier in the same round on the same path — a planted formatting break and a planted unused variable were each reported, then restored (gates-results.json) — so the clean results here are measurements, not an unmatched glob.
F3 — design doc has no Chinese counterpart (Suggestion, pre-existing gap, widened here)
docs/design/2026-08-29-pnpm-worktree-bootstrap.md gained 14 lines (net +12) of English-only prose about the new hook behaviour; ….zh-CN.md still does not exist (ls docs/design/ | grep -i pnpm returns the English file alone). AGENTS.md requires both languages with reciprocal links, kept synchronised in the same change. Per this repo's review rules a translation gap alone is a Suggestion, and the gap predates this PR — but this head widens it. The English prose itself is accurate against the code as measured: the three skip conditions, the two fail-closed conditions and the .gitignore-based cleanliness claim all match the cells above. One omission: it does not mention commit 4's third failure path (could not read core.hooksPath), which is a behaviour change a reader of the doc would not learn about.
F4 — one Reviewer Test Plan step is configuration-dependent (Suggestion, plan text)
Step 1 of How to verify — "Create a fresh linked worktree, run the documented pnpm worktree bootstrap, and confirm that the pre-commit hook is generated and executable" — passes only when the shared config already carries core.hooksPath=.husky/_ (cell 02). With the key unset the bootstrap deliberately skips husky and prints why (cell 03), so the step cannot be performed as written; this lane's own checkout is in the unset state, so a maintainer following the plan in a CI-like checkout hits the skip on the first attempt. Step 2 ("a custom hooks path and HUSKY=0 are left unchanged") is reproducible as written and passes (cells 05/05b/06).
Full plan walk: step 1 → passes in the reported configuration (02, and 23 in the repo's own worktree layout), not reproducible in the unset configuration (03); step 2 → passes (05, 05b, 06); body claim "fails if Husky reports success without configuring the expected hooks path" → passes (09 with a stub, 24 with a husky that writes wrappers only); body claim "preserves an existing custom hooks path, respects the HUSKY=0 opt-out" → passes for a custom path (05) and for HUSKY=0 in a healthy checkout (06), fails for HUSKY=0 against a refused config read (18, see N1); body claim about tree cleanliness → passes (02, 07, 13, 14, 23 all clean, via the .gitignore husky generates).
N2 — the ownership predicate compares two strings whose form depends on where you stand; the error direction is fail-safe, and no reachable configuration turns it into a wrong hook decision (informational, new at this head)
Commit 3 decides ownership with gitDir === commonDir over the two lines of git rev-parse --git-dir --git-common-dir. Git does not print those two in the same form: from a worktree root both are .git/.git or both absolute, but from any directory below a root --git-dir becomes absolute while --git-common-dir stays relative. Measured across 13 real layouts (02-revparse-ownership-string-comparison.png, revparse-cwd.log):
| layout | --git-dir / --git-common-dir |
verdict | truth |
|---|---|---|---|
| primary root | .git / .git |
owns | ✓ |
| primary subdir | /abs/.git / ../.git |
linked | ✗ |
| primary subdir (2 deep) | /abs/.git / ../../.git |
linked | ✗ |
| linked worktree root | /abs/.git/worktrees/x / /abs/.git |
linked | ✓ |
| linked worktree subdir | same as its root | linked | ✓ |
--separate-git-dir clone |
equal absolutes | owns | ✓ |
| submodule | equal absolutes | owns | ✓ |
| bare repo | . / . |
owns | ✓ |
| no repository | exit 128 | none | ✓ |
| non-repo dir nested in a repo | /abs/outer/.git / ../.git |
linked | ✗ |
primary root with GIT_DIR=.git |
.git / .git |
owns | ✓ |
| primary root via symlink | .git / .git |
owns | ✓ |
3 mismatches out of 13, all of them a directory that is not a worktree root, all in the same direction: something that owns its config is reported as linked, which makes the bootstrap decline hooks.
Why this does not become a defect, checked rather than assumed. rootDir is fileURLToPath(new URL('..', import.meta.url)) — the parent of scripts/ — so for this repository it is always the worktree root, where the two forms agree. For the mismatch to matter, rootDir would have to be a strict subdirectory of a worktree while also being a project root; and in that case husky could not have worked anyway, because husky's own first check is existsSync('.git') relative to its cwd (index.js:11). Cell 15 measures exactly this and the outcome is benign: exit 0 with the skip notice, no wrappers, and the outer repository's .git/config contains no hooksPath afterwards (verified by reading the file, not by asking the script) — the husky branch, the only code that writes config, was never reached. The dangerous direction — a real linked worktree reported as owns, which would let husky write the shared config — is not reachable: cells 01–06, 19–23 all classify linked correctly, including the .qwen/worktrees/ layout nested inside the primary. So commit 3's predicate never re-opens the hazard commit 2 closed.
What survives is a wording defect, not a decision defect: in cell 15 the notice says "core.hooksPath is unset and this checkout does not own the repository config … Re-run this script here once hooks are installed in the primary checkout", which tells the operator to wait for a primary checkout that does not exist for that path. Comparing resolved paths (realpath) instead of raw strings would make the predicate independent of cwd form; the current behaviour is safe without it.
N3 — mutation-matrix survivors, adjudicated (completeness reporting, not merge conditions)
14 killed / 18 mutants, unmutated control green at 40 passed | 1 skipped, witness 03-mutation-matrix-killed-vs-survived.png, detail in mutants/summary.json. Every survivor is classified rather than left as a bare "survived":
| mutant | what it removes | result | classification |
|---|---|---|---|
| CTRL | whole file → HEAD^1 |
KILLED, 3 tests | vacuity control passes: test.js:266 (log missing "1 1 pnpm exec husky"), :449 and :650 (stdout missing 'skipping Husky') — each on its intended behavioural assertion with an expected/received diff, none on an import or compile error |
| M1 / M2 / M12 | HUSKY=0 clause / custom-path clause / both |
KILLED at :271, :274, :271 |
guards load-bearing; the combination row shows no hidden layering — both singles die alone |
| M3 | the whole ownership skip block | KILLED, 2 tests | commit 2/3's guard pinned |
| M5 / M6 / M7 / M8 | owns / linked / none always; comparison inverted |
all KILLED | commit 3's predicate pinned in all four directions |
| M9 | failed rev-parse → 'owns' (revert the F2 fix) |
KILLED at :494 |
the F2 fix is pinned |
| M10 | fail-closed's on-disk wrapper half | KILLED at :503 |
the artefact half commit 3 added is load-bearing |
| M13 | fold every non-zero exit into "unset" (revert commit 4) | KILLED at :532 |
commit 4's exit-code discrimination is load-bearing |
| M14 | the recovery sentence | KILLED at :452 |
commit 4's notice change is pinned |
| M11 | fail-closed's config-value half | SURVIVED | decidable, so not dead code — cells 24/25 drive a husky that writes wrappers but not the config: head exits 1, the mutant exits 0. Against the pinned husky 9.1.7 that state is unreachable (the config write at index.js:14 precedes wrapper creation at :19-22, so a soft failure produces neither), making the clause redundant defence today and a coverage gap against any husky that reorders. Fixture that would pin it: the stub already in the test with setsHooksPath: false, writesHooks: true, asserting exit 1. |
| M15 | the no-repository wording branch (prints the linked message for 'none') |
SURVIVED | coverage gap: the test asserts only .toContain('skipping Husky'), which both wordings satisfy, so nothing distinguishes the two notices commit 4 wrote. Fixture: assert noRepoRun.stdout contains git could not resolve a repository. |
| M16 | revert the pathValue→envValue extraction |
KILLED — but see N4 | killed by the mutation's own shape, not by an assertion |
| M16b | interface-preserving: collapse envValue() to a case-sensitive read |
SURVIVED | unobservable on Linux by construction; the test that would pin it is skipIf(process.platform !== 'win32'). Nothing to write on this platform. |
| FURTHER-FIX | the F1 candidate patch applied | SURVIVED | the suite pins nothing on the F1 axis — the unpinned-axis signal for F1 |
N4 — a kill that proves nothing, corrected by a finer mutation (informational)
M16 deletes envValue() while envValue('HUSKY') is still called, so the script crashes and the suite goes red on expected 1 to be +0. Run directly, outside vitest (04-m16-kill-is-a-crash-not-an-assertion.png, mutants/m16-direct-run.log):
ReferenceError: envValue is not defined
at install (file:///tmp/m16probe/scripts/setup-worktree.js:104:5)
M16_DIRECT_EXIT=1
A red suite from a crash is indistinguishable from a red suite from a behavioural assertion unless you look, and the difference matters: reported naively, M16 would have claimed the win32 casing extraction is pinned when what is pinned is only that later code calls the function. M16b re-measures the same axis interface-preserving and survives, which is the honest result. The matrix is quoted with M16 attributed to its crash throughout.
Not covered
- Windows and macOS. The win32-only test is
skipIfon Linux and no Windows host is available here, so theenvValue()casing path,corepack.cmd, and the smoke workflow'swindows-latest/macos-latestlegs are all unexercised. N4 shows this axis cannot be pinned on Linux even in principle. - The real monorepo bootstrap. The fixtures use the real pinned pnpm, real husky and real git, but a single-package layout: the repository's own workspace graph,
patches/,postinstall: patch-packageand thepnpm-workspace.yamlsettings are in no cell. pnpm-worktree-smoke.ymlend-to-end on a runner. Cell 07 reproduces its input state (fresh primary checkout, key unset) and its clean-tree assertion; the workflow itself was not executed.- Per-commit attribution. The checkout is shallow and
git rev-list HEAD^1..HEAD^2returns 1 commit against the snapshot's 6 — the shallow-boundary trap value, not a real count. Commits 3 and 4 are not individually reachable, so the aggregateHEAD^1..HEADdiff is what was verified. Commit 3's and commit 4's contributions are separated by their own cells (13/14/17 vs 16/18) and mutants (M5–M9 vs M10/M13/M14), not by per-commit checkouts. - Dubious ownership, constructed for real. Commit 4's message names "a shared pool's dubious-ownership exit 128". This container runs as uid 1000 with no privilege to create a foreign-owned tree, so exit 128 was reached via a corrupt
.git/config(cell 16, a real git refusal with a realfatal:message) and a single-invocation git stub (cell 18). The exit-code handling is identical, but the ownership path itself was not reproduced. - The linked CI failure (
actions/runs/34353187365) — no GitHub token in this lane; not fetched. - Repo-wide
npm run test— the PR touches no package code; out of scope by design. npm run test:scriptsfailures were not re-run on a base worktree. Full run at head: 2 failed | 83 passed (85 files), 15 failed | 2217 passed | 1 skipped (2233 tests). Both failing files are outside this PR's closure (the diff touches 3 files; neither test file referencessetup-worktree), and each cause was reproduced with no PR code involved:check-tui-dep-direction.test.jsdies onEACCES: permission denied, mkdtemp '/__w/qwen-code/qwen-code/.qwen/gate-e2e-XXXXXX'—.qwenisdr-xr-xr-x root rootwhile the suite runs as uid 1000, and a baremkdtempSyncagainst it fails identically;install-script.test.jsdies ondocker buildwith docker absent from the container. Attributed by A/A probe of the failing operation, not by a base-side rerun. The PR's own file passed inside this run.- The rev-parse layout sweep (13 rows) is a characterization, not an assertion set — its 3 mismatches are reported as N2's measurement and are deliberately excluded from
assertions.json; the behavioural consequences are asserted instead (cell 15). verify-capture.mjsworked as wired for all five images; no capture pipeline was reconstructed.
Methodology
Environment: the CI verify container (node:22-bookworm, node v22.23.2, git 2.39.5, corepack 0.34.6 resolving the repo-pinned pnpm 11.24.0, husky 9.1.7), working tree = refs/pull/11496/merge at depth 2, npm ci + npm run build pre-run; no zstd binary in the image, $RUNNER_TEMP unset in this shell.
Why the control needed no base worktree or rebuild. The PR's effective diff is three files, and only one of them is executable: git diff --name-only HEAD^1..HEAD returns the design doc, scripts/setup-worktree.js and its test. package.json and package-lock.json are untouched, so the dependency tree is not part of the change, and no @qwen-code/* workspace package is on the path under test — there are no internal symlinks whose realpath could quietly resolve a "base" cell into head code. The control is therefore the base script bytes (git show HEAD^1:scripts/setup-worktree.js, sha256 819d0eac…) run in the identical fixture, differing from head by nothing but the change under test. Both arms were sha256-verified against the objects they came from before any cell ran.
Fixtures live under /tmp/pr11496-fixture and /tmp/pr11496-cells, outside the repository on purpose: cells 11/12 have no .git and cell 15 relies on git rev-parse walking up, so a fixture nested under this checkout would resolve against qwen-code's own repository and the ownership measurements would be meaningless. A warmed template (package.json + generated pnpm-lock.yaml + the real scripts/pnpm-package.js + a .husky/pre-commit that appends to a trace file) is copied per cell, so each cell's pnpm install --frozen-lockfile --offline is a real ~0.6 s install against a real store rather than a stub. Between the script under test and .husky/_/pre-commit sit only real git, real corepack/pnpm and real husky; the sole stubs are the four kinds the table names (declining-corepack, wrappers-only-husky, refusing-git — which refuses the one config --get core.hooksPath invocation and execs real git for everything else — and gitless). Post-state oracles are always measured with a working toolchain even in cells that stub git or corepack, so a stub never blinds its own oracle.
Harnesses (ab-harness.mjs, git-semantics.mjs, revparse-cwd.mjs, mutants.mjs, parse-mutants.mjs, assert-matrix.mjs, zero-collateral.mjs, gates.mjs, style-gates.mjs, make-assertions.mjs, plus the printer behind each evidence image) and raw per-cell logs (cell-logs/, mutants/, build-at-head.log, typecheck-at-head.log, test-scripts-full.log) are in this directory; ab-results.json, ab-results-2.json, mutants/matrix.json, mutants/summary.json, gates-results.json, style-gates-results.json, git-semantics.json and assertions.json carry every number quoted above, and assertions.json is assembled from the other result files by make-assertions.mjs rather than typed. Mutants and candidate fixes were written into the real working tree and restored byte-identical after each run (sha256 d45c4975… verified before and after every such run, and git status --porcelain empty at the end of each gate script). Evidence images were produced with the repository's own scripts/verify-capture.mjs; no capture pipeline was reconstructed.
Flakiness gate log
rounds=5 files=1 skipped=0
file scripts/tests/package-scripts.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/package-scripts.test.js
per-file results (P=pass F=fail I=infra-exit, one letter per run):
scripts/tests/package-scripts.test.js: PPPPP
verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/package-scripts.test.js: P (exit 0)
round 2 · scripts/tests/package-scripts.test.js: P (exit 0)
round 3 · scripts/tests/package-scripts.test.js: P (exit 0)
round 4 · scripts/tests/package-scripts.test.js: P (exit 0)
round 5 · scripts/tests/package-scripts.test.js: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
chiga0
left a comment
There was a problem hiding this comment.
No blocking findings.
Approval blockers: none.
Triage: Standard — contained fix with comprehensive test coverage.
What I checked:
-
Husky skip conditions — correctly identifies when NOT to run husky:
HUSKY=0env set → skip (respects opt-out)- Custom hooks path set and differs from
.husky/_→ skip (preserves existing config) - No repository (
ownership === 'none') → skip (no config to write) - Linked worktree + unset
core.hooksPath→ skip (would repoint shared config for all worktrees)
-
Config ownership probe (
repositoryConfigOwnership) — correctly distinguishes:'owns':--git-dir === --git-common-dir(primary checkout or--separate-git-dirclone) → husky is safe'linked': linked worktree where config is shared → husky only safe ifcore.hooksPathalready set'none': no repository → skip
-
Fail-closed verification — after husky exec, checks BOTH
core.hooksPath === '.husky/_'AND.husky/_/pre-commitexists. Husky exits 0 on soft failures (.git can't be found, refusedgit configwrite), so both proofs are required. -
envValue()helper — correctly extracts case-insensitive env reads on Windows, factored out frompathValue(). -
Test coverage — comprehensive:
- Normal flow: install + husky exec
HUSKY=0opt-out: only install- Custom hooks path: only install
- Unset HUSKY env: still reaches husky (not treated as disabled)
- Primary checkout: hooks install
- Linked worktree with unset
core.hooksPath: skipped --separate-git-dirclone: hooks install (owns its own config)- No repository: skipped
- Husky configures path but writes no wrappers: fail-closed
- Husky exits 0 writing nothing: fail-closed
- Husky fails with non-zero exit: propagates exit code
- Git refuses to read config (exit 128): surfaces error
-
Cross-check:
qwen-code-ci-botapproved. The Critical from previous round is fixed and pinned by a test that fails without it.
Unreviewed dimensions: Windows/Linux behavior (covered by existing worktree smoke workflow per PR description).
Reviewed with AI assistance.
qqqys
left a comment
There was a problem hiding this comment.
Critical-only scan at b7882567 — both historical Criticals verified fixed in the current code; no new blocking defect. Approving.
Read in full: scripts/setup-worktree.js (183 lines at this head), the ownership and hook-install decision path, and the changed fixtures in scripts/tests/package-scripts.test.js. The design-doc hunk is documentation and carries no runtime surface.
Historical blocking issues — both confirmed fixed, by reading this head rather than the replies
Two [Critical] findings were filed against earlier commits, the second one fix-induced by the first one's remedy. Both are certifies-falsely + regression. I verified each against the code as it stands now.
Round-1 Critical (husky invoked from a linked worktree rewrites the config every sibling worktree shares) — fixed and still fixed. The skip is intact at setup-worktree.js:119-131: when core.hooksPath is unset and the checkout does not own the repository config, husky is never invoked and the script exits with the install result, so the shared config is not written.
Round-2 Critical (ownsRepositoryConfig() inferred ownership from the filesystem shape of .git, misclassifying both ends) — fixed. The .git-shape proxy is gone entirely: neither statSync nor throwIfNoEntry appears anywhere in the file now. Ownership comes from git, at setup-worktree.js:93-102:
const probe = spawnSync('git', ['rev-parse', '--git-dir', '--git-common-dir'], { cwd: rootDir, env, encoding: 'utf8' });
if (probe.status !== 0) return 'none';
const [gitDir, commonDir] = probe.stdout.trim().split(/\r?\n/);
return gitDir === commonDir ? 'owns' : 'linked';That is the discriminator the finding prescribed, and it closes each of the three states it named:
- No
.gitat all →rev-parsefails →'none'→ the skip branch at :120-121 fires and prints "git could not resolve a repository for this checkout; skipping Husky because there is no repository config for it to write", thenexitWithResult(result)on the successful install. The regression — a source tarball or zip checkout turning a clean dependency install intoexit 1blaming Husky — cannot occur, because husky is never reached in that state. .gitas a file wheregit-dir == git-common-dir(a--separate-git-dirclone, a submodule) →'owns'→ husky runs and hooks are installed. The silent skip that left every commit in such a checkout bypassing.husky/pre-commitis gone.- A genuine linked worktree → the two paths differ →
'linked'→ skipped with the config left alone, which is what keeps the round-1 blocker fixed.
The notice text no longer asserts "linked worktree" as the explanation for a .git file; each branch names its own state, as the finding required.
The nested-repository false success is also closed, by a different mechanism than the finding proposed but a sufficient one. A checkout nested in a repository that already carries core.hooksPath=.husky/_ does not match the skip condition, so husky runs and is then held to two proofs at :139-147 — the config value must read .husky/_ and .husky/_/pre-commit must exist on disk — otherwise the script exits 1. Since husky exits 0 on soft failures, requiring both is what stops a run from reporting success while having created nothing.
The two facts the finding said a fix must not violate still hold. expect(hookless.status).toBe(1) and expect(hookless.stderr).toContain('Husky did not install hooks') are present at package-scripts.test.js:509-510, alongside the configOnly assertion at :504.
The fixture constraint was honoured, which is the part most likely to have been missed. The finding warned that .git stand-ins are valid only while the script uses statSync, and that mkdirSync(checkout/'.git') or a hand-written gitdir: file resolves to nothing under rev-parse. No such stand-in remains — a search for mkdirSync on a .git path and for a literal gitdir: returns nothing in the test file. The fixtures now build real layouts with real git: git init --quiet (:386), git worktree add --quiet --detach (:393) and a --separate-git-dir clone (:400), which is exactly the P/L/S set the finding's witness enumerated, and :476 asserts the --separate-git-dir shape installs hooks rather than skipping.
Independent scan of the current code — no Critical
getHooksPath()(:69-85) maps exit 0 to the value, exit 1 or a spawn error toundefined, and fails closed with a diagnostic on any other status. Treating "git unavailable" the same as "key absent" is what prevents a missing git from becoming a spuriousexit 1, and the ownership probe independently reports that shape as'none'. The remaining fail-closed arm needs an already-unreadable git config to trigger, a state in which the hooks decision genuinely should not be guessed.- The decision order in
install()is correct: theHUSKY=0opt-out and an explicit non-huskycore.hooksPathboth exit with the install result before ownership is consulted, so a pre-existing hook configuration is never overridden. - The registry-retry driver at :172-183 is unchanged in substance and only reached on a failed install, since
install()exits on every success path. - Nothing here writes to a shared git config, touches secrets, or can lose work.
spawnSync('git', …)without a shell is fine on Windows, whererunPnpmalready setsshell: process.platform === 'win32'.
CI
Green at this head with nothing attributable to the diff: Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox), web-shell E2E Smoke, both Desktop Shell jobs and all three Install jobs (ubuntu, macOS, windows) pass — the Install legs matter here because a case in this suite smoke-tests the real worktree bootstrap on every supported host. Only review-pr is still in progress, which is not a gate on this review. A fresh sandboxed verification was running at publish time; I did not wait on it.
|
Released in v0.23.3. |








What this PR does
Ensures the optimized pnpm worktree bootstrap installs the repository's Husky hooks after dependency installation. It preserves an existing custom hooks path, respects the
HUSKY=0opt-out, and fails if Husky reports success without configuring the expected hooks path.Why it's needed
The worktree bootstrap intentionally skips the root prepare phase to avoid rebuilding and bundling the project. That also skipped Husky, so a fresh worktree could have
core.hooksPath=.husky/_without the generated hook wrappers. Commits from that worktree then bypassed the Prettier and ESLint pre-commit checks, leaving CI to report formatting failures.Reviewer Test Plan
How to verify
Create a fresh linked worktree, run the documented pnpm worktree bootstrap, and confirm that the pre-commit hook is generated and executable. Confirm that a custom hooks path and
HUSKY=0are left unchanged.Evidence (Before & After)
N/A (non-UI change). Before the fix, the bootstrap completed successfully while the generated pre-commit hook remained absent and a traced commit invoked no hook. After the fix, the bootstrap creates the hook, retains the expected hooks path, and the regression tests cover the normal, opt-out, custom-path, offline, and registry-fallback paths.
Tested on
Environment (optional)
macOS arm64, Node.js 22.22.0, pnpm 11.24.0.
Risk & Scope
Linked Issues
Related CI failure: https://github.com/QwenLM/qwen-code/actions/runs/34353187365/job/102471375065?pr=11270
中文说明
本 PR 做了什么
确保优化后的 pnpm worktree 初始化在依赖安装完成后安装仓库的 Husky hooks。同时保留已有的自定义 hooks 路径、尊重
HUSKY=0的禁用设置,并在 Husky 返回成功但未配置预期 hooks 路径时让初始化失败。为什么需要这个改动
worktree 初始化为了避免重新构建和打包项目,会有意跳过根目录的 prepare 阶段,但这也一并跳过了 Husky。因此,新 worktree 可能已配置
core.hooksPath=.husky/_,却没有生成 hook 包装脚本;从该 worktree 提交时就会绕过 Prettier 和 ESLint 的 pre-commit 检查,最后由 CI 报出格式错误。Reviewer 测试计划
如何验证
创建一个新的 linked worktree,运行文档中的 pnpm worktree 初始化,然后确认 pre-commit hook 已生成且可执行。再确认自定义 hooks 路径和
HUSKY=0设置不会被修改。证据(修复前后)
不适用(非 UI 改动)。修复前,初始化会成功完成,但生成的 pre-commit hook 仍不存在,跟踪提交过程也确认没有调用 hook。修复后,初始化会生成 hook 并保留预期的 hooks 路径;回归测试覆盖正常、禁用、自定义路径、离线安装和联网回退路径。
测试平台
环境(可选)
macOS arm64、Node.js 22.22.0、pnpm 11.24.0。
风险与范围
关联问题
相关 CI 失败:https://github.com/QwenLM/qwen-code/actions/runs/34353187365/job/102471375065?pr=11270